package com.songyang.redislockandlist.core;

import com.songyang.redislockandlist.util.IpUtils;
import com.songyang.redislockandlist.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@Slf4j
public class redisLock {
    private final RedisTemplate redisTemplate;
    private final RedisUtil redisUtil;
    private String threadName = "";//当前线程的名字
    private String redisListKey = "viewList";//消息队列的key
    private String redisLock = "songYangLock";//Redis分布式锁的key
    private long redisLockTime = 10;//Redis锁过期时间
    private Integer maxListSize = 1000;//队列支持最大的消息数量，超过就会抛异常
    private Integer listTopForNotifySize = 15;//没有拿到锁的线程进入队列进行自旋锁时候队列的阈值
    private Integer listTopForNotifyTimes = 150;//进行自旋锁转阻塞的次数
    private CountDownLatch count = null;//待使用..
    private Integer spanTimes = 100;//唤醒线程查看队列的内容，如果没人拿锁了，长时间的空轮询加重Redis负担，所以结束自己的阈值
    public static String MODE_SPAN = "span";//队列工作模式，自旋
    public static String MODE_NOTIFY = "notify";//队列工作模式，阻塞唤醒
    public static String MODE_SPAN_NOTIFY = "span_notify";//队列工作模式，先自旋在阻塞
    private String mode;//队列工作的模式
    private String redisLockValue;//分布式锁的值，默认会重新分配UUID
    private String notifyThreadFlagKey = "notifyThreadFlag";//是否需要创建唤醒线程的标志位
    private String notifyThreadFlagValue = "123";//是否需要创建唤醒线程key的值
    private Integer notifyThreadFlaTime = 2;
    private String watchDogPre = "watchDog";//看门狗key的前缀


    public redisLock(RedisTemplate redisTemplate, String mode) {
        this.redisTemplate = redisTemplate;
        redisUtil = new RedisUtil(this.redisTemplate);
        this.mode = mode;
        //这个步骤是确定只创建一个
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(notifyThreadFlagKey, notifyThreadFlagValue);
//        String flag = (String) redisTemplate.opsForValue().get(notifyThreadFlagKey);
        if (flag)//说明唤醒线程存在的标志位已经没了
            //如果采用阻塞和先自旋再阻塞的方式，先创建唤醒线程
            if (mode.equals(MODE_NOTIFY) || mode.equals(MODE_SPAN_NOTIFY))
                new Thread(() -> {
                    notifyThreadByList();
                }).start();
    }

    public void lock() {
        if (mode.equals(MODE_SPAN))
            //采用自旋锁的方式
            incrTotalViewWithRedisLockBySpin(redisTemplate, redisUtil);
        else if (mode.equals(MODE_NOTIFY))
            //采用阻塞唤醒的方式，如果采用这种方式，请把主函数的run方法打开，该方法为轮询检查队列的状态，进行唤醒
            incrTotalViewWithRedisLockByWaitAndNotify();
        else if (mode.equals(MODE_SPAN_NOTIFY))
            //采用先自旋在阻塞的方法
            incrTotalViewWithRedisLockBySpanAndNotify(redisTemplate, redisUtil);
    }

    public void unlock() {
        if (mode.equals(MODE_SPAN))
            //采用自旋锁的方式
            releaseRedisLock();
        else if (mode.equals(MODE_NOTIFY))
            //采用阻塞唤醒的方式
            releaseRedisLockForWaitAndNotify();
        else if (mode.equals(MODE_SPAN_NOTIFY))
            //采用先自旋在阻塞的方法
            releaseRedisLockForWaitAndSpan();
    }

    private void notifyThreadByList() {
        new Thread(() -> {
            Integer times = 0;
            redisTemplate.opsForValue().set("notifyThreadId", Thread.currentThread().getId());
            String redisListKey = "viewList";
            String serverIp;
            String redisLock = "songYangLock";
            try {
                serverIp = IpUtils.getInnetIp();
                while (true) {
                    //查看有没有人拿到锁
                    String redisValue = (String) redisTemplate.opsForValue().get(redisLock);
                    //等于null说明这个时候没有人拿到锁，就尝试从消息队列里面出栈
                    if (null == redisValue) {
                        //redisTemplate.opsForList().
                        List<String> range = redisTemplate.opsForList().range(redisListKey, -1, -1);
                        //  log.info(range.size()+"");
                        if (null == range || range.size() == 0) {
                            times++;
                            if (times > spanTimes) {
                                redisTemplate.delete(notifyThreadFlagKey);
                                return;
                            }
                            continue;
                        } else times = 0;
                        String getRedisListName = range.get(0);
                        String[] split = getRedisListName.split("-");
                        if (split[0].equals(serverIp)) {//如果下一个出栈的ip是自己的话，就出栈这个信息，通过后面的id去找到这个线程
                            redisTemplate.opsForList().rightPop(redisListKey);//出栈
                            Thread AwakenedThread = findThread(Long.parseLong(split[1]));//通过线程的id去找到这个线程
                            log.info("唤醒线程：当前队列还有" + redisTemplate.opsForList().range(redisListKey, 0, -1).size() + "个元素等待");
                            synchronized (AwakenedThread) {
                                AwakenedThread.notify();//唤醒这个线程
                            }
                            //唤醒执行增长的线程，阻塞自己，等到执行线程来唤醒自己
//                            synchronized (Thread.currentThread()) {
//                                try {
//                                    Thread.currentThread().wait();
//                                } catch (InterruptedException e) {
//                                    e.printStackTrace();
//                                }
//                            }
                        }
                    } else {
                        //log.info("唤醒线程：Redis锁正在被使用...休眠一段时间再进行");
                        sleepWithMILLISECONDS(10);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    public void incrTotalViewWithRedisLockBySpin(RedisTemplate redisTemplate, RedisUtil redisUtil) {
        String threadName = "";
        String redisListName = "";
        redisLockValue = UUID.randomUUID().toString();
        boolean flag = true;//标志位，如果没有成功的自加，就会一直循环
        try {
            threadName = "Ip=" + IpUtils.getInnetIp() + "-ThreadId=" + Thread.currentThread().getId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Thread.currentThread().setName(threadName);
        //线程尝试拿到锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
        if (!result) {
            //如果没有成功地拿到锁，就进入Redis的消息队列，等到唤醒
            List<String> keyList = redisTemplate.opsForList().range(redisListKey, 0, -1);
            if (keyList.size() > maxListSize)
                throw new RuntimeException("阻塞队列已经满了，不允许再插入新的线程");
            //入队操作，从栈的左侧入队
            redisTemplate.opsForList().leftPush(redisListKey, threadName);
            while (flag) {
                //获取队列的最后一个元素，判断最后一个元素是不是自己的线程名称
                List range = redisTemplate.opsForList().range(redisListKey, -1, -1);
                if (range == null) {
                    log.info("队列为空，休眠一秒钟");
                    sleepWithMILLISECONDS(10);
                }
                redisListName = (String) range.get(0);
                //if判断成功，说明队列当中的线程是自己，自己可以拿到锁取执行了
                if (redisListName != null && redisListName.equals(threadName)) {
                    while (true) {
                        //尝试取拿到锁
                        Boolean tryLock = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
                        if (tryLock) {
                            redisTemplate.opsForList().rightPop(redisListKey);//自己已经拿到锁了，从队列出队，跳出自旋锁，执行下面的内容
                            flag = false;//成功拿到锁，不需要循环
                            break;
                        } else {
                            log.info("到了" + threadName + "这个线程，但是没有拿到锁，睡眠一段时间在重新试");
                            sleepWithMILLISECONDS(100);
                        }
                    }
                } else {
                    //队列当中的不是自己休眠一会再去抢占
                    sleepWithMILLISECONDS(100);
                }
            }
        }
    }

    private void sleepWithMILLISECONDS(int milliseconds) {
        try {
            TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void releaseRedisLock() {
        String getValue = (String) redisUtil.get(redisLock);
        if (getValue != null && getValue.equals(redisLockValue))
            redisTemplate.delete(redisLock);//处理成功释放锁
        log.info("逻辑处理成功成功！释放锁");
    }

    private void releaseRedisLockForWaitAndNotify() {
        String getValue = (String) redisUtil.get(redisLock);
        if (getValue != null && getValue.equals(redisLockValue))
            redisTemplate.delete(redisLock);//处理成功释放锁
        log.info("增长线程：逻辑处理成功成功！释放锁");
//        Integer notifyThreadId = (Integer) redisTemplate.opsForValue().get("notifyThreadId");
//        Thread notifyThread = findThread(notifyThreadId);
//
//        if (notifyThread == null) {
//            log.info("增长线程：唤醒 '唤醒线程'失败！");
//            return;
//        }
//        Thread.State state = notifyThread.getState();
//        if (state.toString().equals("WAITING")) {
//            synchronized (notifyThread) {
//                notifyThread.notify();
//            }
//            log.info("增长线程：唤醒 '唤醒线程'成功！");
//        }
    }

    private static Thread findThread(long threadId) {
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        while (group != null) {
            Thread[] threads = new Thread[(int) (group.activeCount() * 1.2)];
            int count = group.enumerate(threads, true);
            for (int i = 0; i < count; i++) {
                if (threadId == threads[i].getId()) {
                    return threads[i];
                }
            }
            group = group.getParent();
        }
        return null;
    }

    private void incrTotalViewWithRedisLockByWaitAndNotify() {
        redisLockValue = UUID.randomUUID().toString();
        try {
            threadName = IpUtils.getInnetIp() + "-" + Thread.currentThread().getId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Thread.currentThread().setName(threadName);
        while (true) {
            //线程尝试拿到锁
            // System.out.println("redisTemplate.opsForValue().get(\"zhang\") = " + redisTemplate.opsForValue().get("zhang"));
            Boolean result = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
            if (!result) {
                //如果没有成功的拿到锁，就进入Redis的消息队列，等到唤醒
                //入队操作，从栈的左侧入队
                // log.info("线程"+Thread.currentThread().getId()+"没有拿到锁，入列");
                redisTemplate.opsForList().leftPush(redisListKey, threadName);
                // log.info("当前队列还有"+redisTemplate.opsForList().range(redisListKey,0,-1).size()+"个元素等待");
                //入队之后进入阻塞，等待其他线程唤醒。
                synchronized (Thread.currentThread()) {
                    try {
                        Thread.currentThread().wait();//进入阻塞状态
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else//成功拿到锁，跳出循环，开始执行逻辑
                break;
        }
        log.info("增长线程：" + threadName + "成功拿到锁执行增长访问量！");
//        incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
//        releaseRedisLockForWaitAndNotify(redisLock, redisLockValue);
        //  log.info("一共执行："+(System.currentTimeMillis()-start)+"ms时间");
    }

    private void incrTotalViewWithRedisLockBySpanAndNotify(RedisTemplate redisTemplate, RedisUtil redisUtil) {
        //思路：当一个线程没有拿到锁的时候，先自旋一段时间，或者队列内容不大的时候进行自旋
        //如果自旋的时间超过阈值或者队列的内容已经很多了，就直接阻塞
        Integer times = 0;
        Boolean flag = true;
        redisLockValue = UUID.randomUUID().toString();
        try {
            threadName = IpUtils.getInnetIp() + "-" + Thread.currentThread().getId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Thread.currentThread().setName(threadName);
        while (flag) {
            //线程尝试拿到锁
            Boolean result = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
            if (!result) {//没有拿到锁
                //先进行自旋一会
                //当前队列的长度不是特别大
                if (redisTemplate.opsForList().range(redisListKey, 0, -1).size() < listTopForNotifySize) {
                    while (true) {
                        Boolean spanResult = redisTemplate.opsForValue().setIfAbsent(redisLock, redisLockValue, redisLockTime, TimeUnit.SECONDS);
                        if (!spanResult) {//自旋的时候没有拿到锁
                            sleepWithMILLISECONDS(110);
                            times++;
                        } else {//成功拿到锁
                            flag = false;
//                            incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
//                            releaseRedisLockForWaitAndSpan(redisLock, redisLockValue);//释放锁
                            break;
                        }
                        if (times > listTopForNotifyTimes) {//说明自旋的次数到了，要入队阻塞
                            log.info(threadName + "自旋失败，进行阻塞");
                            times = 0;
                            waitAndPush(threadName);//阻塞入队
                            break;//当他被唤醒的时候，要跳出这个循环
                        }
                    }
                } else //如果队列很长了，就直接阻塞自己
                    waitAndPush(threadName);
            } else//成功拿到锁，跳出循环，开始执行逻辑
                break;
        }
//        log.info("增长线程：" + threadName + "成功拿到锁执行增长访问量！");
//        incrTotalView(redisUtil);//拿到锁以后在进行自增访问量
//        releaseRedisLock(redisLock, redisLockValue);
//        releaseRedisLockForWaitAndSpan(redisLock, redisLockValue);
    }

    private void releaseRedisLockForWaitAndSpan() {
        String getValue = (String) redisUtil.get(redisLock);
        if (getValue != null && getValue.equals(redisLockValue)) {
            Boolean delete = redisTemplate.delete(redisLock);//处理成功释放锁
        }
        log.info("增长线程：逻辑处理成功成功！释放锁");
//        Integer notifyThreadId = (Integer) redisTemplate.opsForValue().get("notifyThreadId");
//        Thread notifyThread = findThread(notifyThreadId);
//
//        if (notifyThread == null) {
//            log.info("增长线程：唤醒 '唤醒线程'失败！");
//            return;
//        }
//        Thread.State state = notifyThread.getState();
//        if (state.toString().equals("WAITING")) {
//            synchronized (notifyThread) {
//                notifyThread.notify();
//            }
//            log.info("增长线程：唤醒 '唤醒线程'成功！");
//        }
    }

    //入队并且阻塞
    private void waitAndPush(String threadName) {
        synchronized (Thread.currentThread()) {
            try {
                redisTemplate.opsForList().leftPush(redisListKey, threadName);//入队
                Thread.currentThread().wait();//进入阻塞状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void createWatchDog(Integer threadId) {
        new Thread(() -> {
            redisTemplate.opsForValue().set(watchDogPre + threadId, 1);//创建看门狗
            Timer timer = new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    //自增一下，查看当前看门狗下面的数量
                    Long times = redisTemplate.opsForValue().increment(watchDogPre + threadId);
                    //如果太久没有清理这个看门狗的状态，说明线程挂掉了，删除锁，结束线程，结束自己。
                    if (times > 5) {
                        redisTemplate.delete(redisLock);//删除锁
                        redisTemplate.delete(watchDogPre + threadId);//删除自己在Redis的key
                        Thread thread = findThread(threadId);
                        if (null != thread) thread.stop();//结束线程
                        timer.cancel();//结束自己定时任务
                        return;
                    } else redisTemplate.expire(redisLock, redisLockTime, TimeUnit.SECONDS);//给这把锁续命
                    System.out.println(threadId + "线程启动看门狗");
                }
            }, 1000, 1000);//一秒后执行，之后每隔两秒执行一次
        }).start();
    }

    private void feedDog(Integer threadId) {
        if ((Integer) redisTemplate.opsForValue().get(watchDogPre + threadId) >= 0)
            redisTemplate.opsForValue().decrement(watchDogPre + threadId);//喂狗，如果次数大于0就自减一次
    }
}
